dhcpv6: support a configurable DUID
authorDavid Härdeman <[email protected]>
Fri, 10 Oct 2025 07:38:38 +0000 (09:38 +0200)
committerÁlvaro Fernández Rojas <[email protected]>
Sun, 26 Oct 2025 21:29:45 +0000 (22:29 +0100)
Allow the use of a (stable) globally configured DUID as the server identifier.

Currently, odhcpd generates a per-interface DUID-LL, meaning that the DUID is
not stable across interfaces and will change if/when hardware changes.

Supporting a stable DUID brings odhcpd's behaviour in line with RFC8415, §11:

                                                      ...The DUID is
   designed to be unique across all DHCP clients and servers, and stable
   for any specific client or server.  That is, the DUID used by a
   client or server SHOULD NOT change over time if at all possible; for
   example, a device's DUID should not change as a result of a change in
   the device's network hardware or changes to virtual interfaces...

Signed-off-by: David Härdeman <[email protected]>
Link: https://github.com/openwrt/odhcpd/pull/274
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
README.md
src/config.c
src/dhcpv6-ia.c
src/dhcpv6.c
src/odhcpd.h

index 65787d77bd199bd02963e6ea1cc296692aaf3df8..5d63cd469dc929344e1da5b40214cdb639db818b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -138,6 +138,14 @@ and may also receive information from ubus
 | url          |string | yes   | e.g. `tftp://[fd11::1]/pxe.efi` |
 | arch         |integer| no    | the arch code. `07` is EFI. If not present, this boot6 will be the default. |
 
+odhcpd also uses the UCI configuration file `/etc/config/network` for configuration
+of the following options:
+
+### Section of type globals
+| Option            | Type     |Required|Description |
+| :----------------    | :---- | :---- | :---------- |
+| dhcp_default_duid |string | no       | The DUID to use to identify the DHCPv6 server to clients. |
+
 
 ### System variables for Timezone options (uci system.system)
 | Option  | Type  |Required|Description |
index 7bff307060e5e0439d41238acb6c09d1725a8526..654c329dfc86bc352a816a7e139a5f3df4b18744 100644 (file)
@@ -45,6 +45,8 @@ struct config config = {
        .log_level = LOG_WARNING,
        .log_level_cmdline = false,
        .log_syslog = true,
+       .default_duid = { 0 },
+       .default_duid_len  = 0,
 };
 
 struct sys_conf sys_conf = {
@@ -260,6 +262,20 @@ const struct uci_blob_param_list system_attr_list = {
        .params = system_attrs,
 };
 
+enum {
+       GLOBAL_ATTR_DUID,
+       GLOBAL_ATTR_MAX
+};
+
+static const struct blobmsg_policy global_attrs[GLOBAL_ATTR_MAX] = {
+       [GLOBAL_ATTR_DUID] = { .name = "dhcp_default_duid", .type = BLOBMSG_TYPE_STRING },
+};
+
+const struct uci_blob_param_list global_attr_list = {
+       .n_params = GLOBAL_ATTR_MAX,
+       .params = global_attrs,
+};
+
 static const struct { const char *name; uint8_t flag; } ra_flags[] = {
        { .name = "managed-config", .flag = ND_RA_FLAG_MANAGED },
        { .name = "other-config", .flag = ND_RA_FLAG_OTHER },
@@ -415,6 +431,26 @@ static int parse_ra_flags(uint8_t *flags, struct blob_attr *attr)
        return 0;
 }
 
+static void set_global_config(struct uci_section *s)
+{
+       struct blob_attr *tb[GLOBAL_ATTR_MAX], *c;
+
+       blob_buf_init(&b, 0);
+       uci_to_blob(&b, s, &global_attr_list);
+       blobmsg_parse(global_attrs, GLOBAL_ATTR_MAX, tb, blob_data(b.head), blob_len(b.head));
+
+       if ((c = tb[GLOBAL_ATTR_DUID])) {
+               size_t len = blobmsg_data_len(c) / 2;
+
+               config.default_duid_len = 0;
+               if (len >= DUID_MIN_LEN && len <= DUID_MAX_LEN) {
+                       ssize_t r = odhcpd_unhexlify(config.default_duid, len, blobmsg_get_string(c));
+                       if (r >= DUID_MIN_LEN)
+                               config.default_duid_len = r;
+               }
+       }
+}
+
 static void set_config(struct uci_section *s)
 {
        struct blob_attr *tb[ODHCPD_ATTR_MAX], *c;
@@ -2227,6 +2263,7 @@ void odhcpd_reload(void)
        struct interface *master = NULL, *i, *tmp;
        char *uci_dhcp_path = "dhcp";
        char *uci_system_path = "system";
+       char *uci_network_path = "network";
 
        if (!uci)
                return;
@@ -2238,17 +2275,32 @@ void odhcpd_reload(void)
                sprintf(uci_dhcp_path, "%s/dhcp", config.uci_cfgdir);
                uci_system_path = alloca(dlen + sizeof("/system"));
                sprintf(uci_system_path, "%s/system", config.uci_cfgdir);
+               uci_network_path = alloca(dlen + sizeof("/network"));
+               sprintf(uci_network_path, "%s/network", config.uci_cfgdir);
        }
 
        vlist_update(&leases);
        avl_for_each_element(&interfaces, i, avl)
                clean_interface(i);
 
+       struct uci_package *network = NULL;
+       if (!uci_load(uci, uci_network_path, &network)) {
+               struct uci_element *e;
+
+               /* 0. Global settings */
+               uci_foreach_element(&network->sections, e) {
+                       struct uci_section *s = uci_to_section(e);
+                       if (!strcmp(s->type, "globals"))
+                               set_global_config(s);
+               }
+       }
+       uci_unload(uci, network);
+
        struct uci_package *dhcp = NULL;
        if (!uci_load(uci, uci_dhcp_path, &dhcp)) {
                struct uci_element *e;
 
-               /* 1. Global settings */
+               /* 1. General odhcpd settings */
                uci_foreach_element(&dhcp->sections, e) {
                        struct uci_section *s = uci_to_section(e);
                        if (!strcmp(s->type, "odhcpd"))
@@ -2284,7 +2336,7 @@ void odhcpd_reload(void)
        if (config.enable_tz && !uci_load(uci, uci_system_path, &system)) {
                struct uci_element *e;
 
-               /* 1. System settings */
+               /* 5. System settings */
                uci_foreach_element(&system->sections, e) {
                        struct uci_section *s = uci_to_section(e);
                        if (!strcmp(s->type, "system"))
index 2948607dbba25cacc347ae7b37b2c6941ef09f47..b2efd4c45a7b1f7965cee8f88d238ce15cbb7d6a 100644 (file)
@@ -191,10 +191,15 @@ static int send_reconf(struct dhcp_assignment *assign)
                .key = { 0 },
        };
 
-       uint8_t duid_ll_hdr[] = { 0x00, 0x03, 0x00, 0x01 };
-       memcpy(serverid.data, duid_ll_hdr, sizeof(duid_ll_hdr));
-       odhcpd_get_mac(iface, &serverid.data[sizeof(duid_ll_hdr)]);
-       serverid.len = htons(sizeof(duid_ll_hdr) + ETH_ALEN);
+       if (config.default_duid_len > 0) {
+               memcpy(serverid.data, config.default_duid, config.default_duid_len);
+               serverid.len = htons(config.default_duid_len);
+       } else {
+               uint16_t duid_ll_hdr[] = { htons(DUID_TYPE_LL), htons(ARPHRD_ETHER) };
+               memcpy(serverid.data, duid_ll_hdr, sizeof(duid_ll_hdr));
+               odhcpd_get_mac(iface, &serverid.data[sizeof(duid_ll_hdr)]);
+               serverid.len = htons(sizeof(duid_ll_hdr) + ETH_ALEN);
+       }
 
        memcpy(clientid.data, assign->clid_data, assign->clid_len);
 
index 72e1855b3c02df22da2e8ad76ba8a5128df59c22..ab55ff6508c2bdc7b81a35410a8a61a0ec6bcd44 100644 (file)
@@ -378,10 +378,15 @@ static void handle_client_request(void *addr, void *data, size_t len,
                .serverid_buf = { 0 },
        };
 
-       uint8_t duid_ll_hdr[] = { 0x00, 0x03, 0x00, 0x01 };
-       memcpy(dest.serverid_buf, duid_ll_hdr, sizeof(duid_ll_hdr));
-       odhcpd_get_mac(iface, &dest.serverid_buf[sizeof(duid_ll_hdr)]);
-       dest.serverid_length = htons(sizeof(duid_ll_hdr) + ETH_ALEN);
+       if (config.default_duid_len > 0) {
+               memcpy(dest.serverid_buf, config.default_duid, config.default_duid_len);
+               dest.serverid_length = htons(config.default_duid_len);
+       } else {
+               uint16_t duid_ll_hdr[] = { htons(DUID_TYPE_LL), htons(ARPHRD_ETHER) };
+               memcpy(dest.serverid_buf, duid_ll_hdr, sizeof(duid_ll_hdr));
+               odhcpd_get_mac(iface, &dest.serverid_buf[sizeof(duid_ll_hdr)]);
+               dest.serverid_length = htons(sizeof(duid_ll_hdr) + ETH_ALEN);
+       }
 
        struct _packed {
                uint16_t type;
index f2e2e74ca1b9e896d1d1ab87d143d8e4e303a6df..55107e509a08a466a07aa5a4c7faa9be6ac47667 100644 (file)
@@ -183,6 +183,19 @@ enum odhcpd_assignment_flags {
        OAF_DHCPV6_PD           = (1 << 6),
 };
 
+/* 2-byte type + 128-byte DUID, RFC8415, §11.1 */
+#define DUID_MAX_LEN 130
+/* In theory, 2 (type only), or 7 (DUID-EN + 1-byte data), but be reasonable */
+#define DUID_MIN_LEN 10
+#define DUID_HEXSTRLEN (DUID_MAX_LEN * 2 + 1)
+
+enum duid_type {
+       DUID_TYPE_LLT = 1,
+       DUID_TYPE_EN = 2,
+       DUID_TYPE_LL = 3,
+       DUID_TYPE_UUID = 4,
+};
+
 struct config {
        bool legacy;
        bool enable_tz;
@@ -198,6 +211,9 @@ struct config {
        int log_level;
        bool log_level_cmdline;
        bool log_syslog;
+
+       uint8_t default_duid[DUID_MAX_LEN];
+       size_t default_duid_len;
 };
 
 struct sys_conf {
@@ -207,10 +223,6 @@ struct sys_conf {
        size_t tzdb_tz_len;
 };
 
-/* 2-byte type + 128-byte DUID, RFC8415, §11.1 */
-#define DUID_MAX_LEN 130
-#define DUID_HEXSTRLEN (DUID_MAX_LEN * 2 + 1)
-
 struct duid {
        uint8_t len;
        uint8_t id[DUID_MAX_LEN];